package za.org.jcicada.comms.impl.tcp;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import za.org.jcicada.comms.IAcceptListener;
import za.org.jcicada.comms.IConnection;
import za.org.jcicada.comms.exceptions.CommsException;
import za.org.jcicada.comms.impl.AcceptorBase;

/**
 * Listen for clients connecting to a TCP server socket and accept local
 * TcpConnections.
 */
public class TcpAcceptor extends AcceptorBase {

	private static final Log logger = LogFactory.getLog(TcpAcceptor.class);
	public static final String ANY_ADDR = "ANYADDR";
	public static final String TCP = "tcp";
	protected ServerSocket serverSocket;
	
	public TcpAcceptor(IAcceptListener acceptListener, URI baseLocationURI) throws IOException {
		super(acceptListener);
		if (baseLocationURI.getHost().equals(ANY_ADDR)) {
			initialize(new InetSocketAddress(baseLocationURI.getPort()));
		} else {
			initialize(new InetSocketAddress(baseLocationURI.getHost(), baseLocationURI.getPort()));
		}
	}

	public TcpAcceptor(IAcceptListener acceptListener, int port) throws IOException {
		this(acceptListener, new InetSocketAddress(port));
	}

	public TcpAcceptor(IAcceptListener acceptListener, String host, int port) throws IOException {
		this(acceptListener, new InetSocketAddress(host, port));
	}

	public TcpAcceptor(IAcceptListener acceptListener, InetSocketAddress address) throws IOException {
		super(acceptListener);
		initialize(address);
	}

	private void initialize(InetSocketAddress address) throws IOException {
		ServerSocketChannel serverChannel = ServerSocketChannel.open();
		serverChannel.configureBlocking(false);
		serverSocket = serverChannel.socket();
		serverSocket.setReuseAddress(true);
		serverSocket.bind(address);
		TcpSelector tcpSelector = TcpSelector.getInstance();
		tcpSelector.addAcceptor(serverChannel, this);
	}

	@Override
	public void close() throws IOException {
		logger.info(toString() + " closing");
		serverSocket.close();
		super.close();
		logger.info(super.toString() + " has closed");
	}
	
	@Override
	public boolean isClosed() {
		return serverSocket.isClosed();
	}

	/**
	 * Created an IConnection object for a given channel. Factored out as a
	 * separate method so that subclasses can override it.
	 * 
	 * @param channel
	 *            SocketChannel to use for connection.
	 */
	protected IConnection createConnection(SocketChannel channel) throws CommsException {
		return new TcpConnection(channel);
	}

	/**
	 * Determine the actual port the acceptor is listening on (used if 0 is
	 * given for the port number).
	 */
	public int getLocalPort() {
		return serverSocket.getLocalPort();
	}

	@Override
	public String getProtocol() {
		return TCP;
	}

	@Override
	public URI getBaseLocationURI() {
		if (serverSocket.getInetAddress().isAnyLocalAddress()) {
			try {
				return URI.create(String.format("%s://%s:%d", getProtocol(), InetAddress.getLocalHost().getHostAddress(), getLocalPort()));
			} catch (UnknownHostException ex) {
				logger.fatal("Error trying to determine host address.", ex);
			}
			return null;
		} else {
			return URI.create(String.format("%s:/%s", getProtocol(), serverSocket.getLocalSocketAddress()));
		}
	}

	/**
	 * Handle an accept event on this server socket channel by creating a new
	 * connection object and adding it to the list of open connections.
	 * 
	 * @param socketChannel newly accepted socket channel to be wrapped in a connection
	 */
	void accept(SocketChannel socketChannel) throws Exception {
		logger.info(String.format("Client connected to %s from %s", this, socketChannel.socket().getRemoteSocketAddress()));
		TcpConnection queue = (TcpConnection) createConnection(socketChannel);
		addQueue(queue);
	}
	
	@Override
	public String toString() {
		return super.toString() + ":" + serverSocket.getLocalSocketAddress();
	}

}
